package com.vladsch.flexmark.util.format;

import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.ast.TextCollectingVisitor;
import com.vladsch.flexmark.util.collection.MaxAggregator;
import com.vladsch.flexmark.util.collection.MinAggregator;
import com.vladsch.flexmark.util.data.DataHolder;
import com.vladsch.flexmark.util.format.options.DiscretionaryText;
import com.vladsch.flexmark.util.html.CellAlignment;
import com.vladsch.flexmark.util.misc.*;
import com.vladsch.flexmark.util.sequence.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;

import static com.vladsch.flexmark.util.format.TableCell.DEFAULT_CELL;
import static com.vladsch.flexmark.util.format.TableCell.NOT_TRACKED;
import static com.vladsch.flexmark.util.format.options.DiscretionaryText.ADD;
import static com.vladsch.flexmark.util.misc.Utils.*;

@SuppressWarnings("WeakerAccess")
public class MarkdownTable {

    final public TableSection header;
    final public TableSection separator;
    final public TableSection body;
    final public TableSection caption;
    public TableFormatOptions options;

    private boolean isHeading;
    private boolean isSeparator;
    CharSequence formatTableIndentPrefix;

    // used by finalization and conversion to text
    private CellAlignment[] alignments;
    private int[] columnWidths;

    // generated by conversion to text
    final private @NotNull ArrayList<TrackedOffset> trackedOffsets = new ArrayList<>();

    final private TableSection[] ALL_SECTIONS;      // includes  header, separator, body, caption
    final private TableSection[] ALL_TABLE_ROWS;    // includes header, separator, body
    final private TableSection[] ALL_CONTENT_ROWS;  // header, body
    final private TableSection[] ALL_HEADER_ROWS;   // header
    final private TableSection[] ALL_BODY_ROWS;     // body
    final public static CharPredicate COLON_TRIM_CHARS = CharPredicate.anyOf(':');
    final private CharSequence tableChars;

    final public static NumericSuffixPredicate NO_SUFFIXES = s -> false;
    final public static NumericSuffixPredicate ALL_SUFFIXES_SORT = s -> true;
    final public static NumericSuffixPredicate ALL_SUFFIXES_NO_SORT = new NumericSuffixPredicate() {
        @Override
        public boolean test(String s) {
            return true;
        }

        @Override
        public boolean sortSuffix(@NotNull String suffix) {
            return false;
        }
    };

    public MarkdownTable(@NotNull CharSequence tableChars, @Nullable DataHolder options) {
        this(tableChars, new TableFormatOptions(options));
    }

    public MarkdownTable(@NotNull CharSequence tableChars, @Nullable TableFormatOptions options) {
        this.tableChars = tableChars;
        this.formatTableIndentPrefix = options == null ? "" : options.formatTableIndentPrefix;
        header = new TableSection(TableSectionType.HEADER);
        separator = new TableSeparatorSection(TableSectionType.SEPARATOR);
        body = new TableSection(TableSectionType.BODY);
        caption = new TableCaptionSection(TableSectionType.CAPTION);
        isHeading = true;
        isSeparator = false;
        this.options = options == null ? new TableFormatOptions(null) : options;

        ALL_SECTIONS = new TableSection[] { header, separator, body, caption };
        ALL_TABLE_ROWS = new TableSection[] { header, separator, body };
        ALL_CONTENT_ROWS = new TableSection[] { header, body };
        ALL_HEADER_ROWS = new TableSection[] { header };
        ALL_BODY_ROWS = new TableSection[] { body };
    }

    public CharSequence getTableChars() {
        return tableChars;
    }

    public TableCell getCaptionCell() {
        return caption.rows.size() > 0 && caption.rows.get(0).cells.size() > 0 ? caption.rows.get(0).cells.get(0) : TableCaptionSection.NULL_CELL;
    }

    public CharSequence getFormatTableIndentPrefix() {
        return formatTableIndentPrefix;
    }

    public void setFormatTableIndentPrefix(CharSequence formatTableIndentPrefix) {
        this.formatTableIndentPrefix = formatTableIndentPrefix;
    }

    public void setCaptionCell(TableCell captionCell) {
        if (caption.rows.size() == 0) {
            caption.rows.add(caption.defaultRow());
        }

        caption.rows.get(0).cells.clear();
        caption.rows.get(0).cells.add(captionCell);
    }

    public BasedSequence getCaption() {
        return getCaptionCell().text;
    }

    public void setCaption(CharSequence caption) {
        TableCell captionCell = getCaptionCell();
        setCaptionCell(captionCell.withText(captionCell.openMarker.isEmpty() ? "[" : captionCell.openMarker, caption, captionCell.closeMarker.isEmpty() ? "]" : captionCell.closeMarker));
    }

    /*
     * Used by visitor during table creation
     *
     */
    public void setCaptionWithMarkers(
            Node tableCellNode,
            CharSequence captionOpen,
            CharSequence caption,
            CharSequence captionClose
    ) {
        setCaptionCell(new TableCell(tableCellNode, captionOpen, options.formatTableCaptionSpaces == DiscretionaryText.AS_IS ? caption : BasedSequence.of(caption).trim(), captionClose, 1, 1));
    }

    public int getHeadingRowCount() {
        return header.rows.size();
    }

    public int getSeparatorRowCount() {
        return separator.rows.size();
    }

    public int getBodyRowCount() {
        return body.rows.size();
    }

    public int getCaptionRowCount() {
        return caption.rows.size();
    }

    public int getMaxHeadingColumns() {
        return header.getMaxColumns();
    }

    public int getMaxSeparatorColumns() {
        return separator.getMaxColumns();
    }

    public int getMaxBodyColumns() {
        return body.getMaxColumns();
    }

    public boolean getHaveCaption() {
        return caption.rows.size() > 0 && caption.rows.get(0).cells.size() > 0 && caption.rows.get(0).cells.get(0).columnSpan != 0;
    }

    public int getMinColumns() {
        int headingColumns = header.getMinColumns();
        int separatorColumns = separator.getMinColumns();
        int bodyColumns = body.getMinColumns();
        return min(headingColumns == 0 ? Integer.MAX_VALUE : headingColumns, separatorColumns, bodyColumns == 0 ? Integer.MAX_VALUE : bodyColumns);
    }

    public int getMaxColumns() {
        int headingColumns = header.getMaxColumns();
        int separatorColumns = separator.getMaxColumns();
        int bodyColumns = body.getMaxColumns();
        return max(headingColumns, separatorColumns, bodyColumns);
    }

    public int getMinColumnsWithoutColumns(boolean withSeparator, int... skipColumns) {
        return aggregateTotalColumnsWithoutColumns(withSeparator ? ALL_TABLE_ROWS : ALL_CONTENT_ROWS, MinAggregator.INSTANCE, skipColumns);
    }

    public int getMaxColumnsWithoutColumns(boolean withSeparator, int... skipColumns) {
        return aggregateTotalColumnsWithoutColumns(withSeparator ? ALL_TABLE_ROWS : ALL_CONTENT_ROWS, MaxAggregator.INSTANCE, skipColumns);
    }

    public int getMinColumnsWithoutRows(boolean withSeparator, int... skipRows) {
        return aggregateTotalColumnsWithoutRows(withSeparator ? ALL_TABLE_ROWS : ALL_CONTENT_ROWS, MinAggregator.INSTANCE, skipRows);
    }

    public int getMaxColumnsWithoutRows(boolean withSeparator, int... skipRows) {
        return aggregateTotalColumnsWithoutRows(withSeparator ? ALL_TABLE_ROWS : ALL_CONTENT_ROWS, MaxAggregator.INSTANCE, skipRows);
    }

    @NotNull
    public List<TrackedOffset> getTrackedOffsets() {
        return trackedOffsets;
    }

    @Nullable
    private TrackedOffset findTrackedOffset(int offset) {
        for (TrackedOffset trackedOffset : trackedOffsets) {
            if (trackedOffset.getOffset() == offset) return trackedOffset;
            if (trackedOffset.getOffset() > offset) break;
        }
        return null;
    }

    @Nullable
    public TrackedOffset getTrackedOffset(int offset) {
        return findTrackedOffset(offset);
    }

    public int getTrackedOffsetIndex(int offset) {
        TrackedOffset trackedOffset = findTrackedOffset(offset);
        return trackedOffset == null ? offset : trackedOffset.getIndex();
    }

    public int getTableStartOffset() {
        // MdNav:diagnostic/4134, index out of range
        List<TableRow> rows = getAllRows();
        if (!rows.isEmpty()) {
            TableRow row = rows.get(0);
            row.normalizeIfNeeded();

            if (row.cells.size() > 0) {
                return row.cells.get(0).getStartOffset(null);
            }
        }
        return 0;
    }

    public TableCellOffsetInfo getCellOffsetInfo(int offset) {
        int r = 0;
        for (TableRow row : getAllSectionRows()) {
            row.normalizeIfNeeded();
            TableCell lastCell = row.cells.get(row.cells.size() - 1);
            BasedSequence lastSegment = lastCell.getLastSegment();
            int lineEndOffset = lastSegment.baseEndOfLineAnyEOL();
            if (lineEndOffset == -1) lineEndOffset = lastSegment.getEndOffset();

            if (offset <= lineEndOffset) {
                // it is on this line

                int i = 0;
                TableCell previousCell = null;
                for (TableCell cell : row.cells) {
                    if (!cell.closeMarker.isEmpty() ? offset < cell.closeMarker.getEndOffset() : offset <= cell.text.getEndOffset()) {
                        if (offset >= cell.getInsideStartOffset(previousCell) && offset <= cell.getInsideEndOffset()) {
                            // in the cell area
                            return new TableCellOffsetInfo(offset, this, getAllRowsSection(r), row, cell, r, i, i, offset - cell.getInsideStartOffset(previousCell));
                        } else {
                            // it the span area or before pipe of first cell
                            return new TableCellOffsetInfo(offset, this, getAllRowsSection(r), row, cell, r, i, null, null);
                        }
                    }
                    i++;
                    previousCell = cell;
                }
                // after the last cell
                return new TableCellOffsetInfo(offset, this, getAllRowsSection(r), row, lastCell, r, i, null, null);
            }
            r++;
        }

        TableSection lastSection = getAllRowsSection(r - 1);
        return new TableCellOffsetInfo(offset, this, lastSection, null, null, r, 0, null, null);
    }

    /**
     * @deprecated Use {@link #addTrackedOffset(TrackedOffset)}
     *         To create: TrackedOffset.track(offset)
     */
    @Deprecated
    public boolean addTrackedOffset(int offset) {
        return addTrackedOffset(TrackedOffset.track(offset, null, false));
    }

    /**
     * @deprecated Use {@link #addTrackedOffset(TrackedOffset)}
     *         To create: TrackedOffset.track(offset, afterSpace)
     */
    @Deprecated
    public boolean addTrackedOffset(int offset, boolean afterSpace) {
        return addTrackedOffset(TrackedOffset.track(offset, afterSpace ? ' ' : null, false));
    }

    /**
     * @deprecated Use {@link #addTrackedOffset(TrackedOffset)}
     *         To create: TrackedOffset.track(offset, afterSpace, afterDelete)
     */
    @Deprecated
    public boolean addTrackedOffset(int offset, boolean afterSpace, boolean afterDelete) {
        return addTrackedOffset(TrackedOffset.track(offset, afterSpace ? ' ' : null, afterDelete));
    }

    /**
     * @deprecated Use {@link #addTrackedOffset(TrackedOffset)}
     *         To create: TrackedOffset.track(offset, c, afterDelete)
     */
    @Deprecated
    public boolean addTrackedOffset(int offset, Character c, boolean afterDelete) {
        return addTrackedOffset(TrackedOffset.track(offset, c, afterDelete));
    }

    public boolean addTrackedOffset(@NotNull TrackedOffset trackedOffset) {
        int offset = trackedOffset.getOffset();
        trackedOffsets.removeIf(it -> it.getOffset() == offset);
        trackedOffsets.add(trackedOffset);

        TableCellOffsetInfo info = getCellOffsetInfo(offset);
        if (info.getInsideColumn()) {
            // real cell, we can add it to the cells contents
            info.tableRow.cells.set(info.column,
                    info.tableCell.withTrackedOffset(
                            offset - info.tableCell.getTextStartOffset(info.column == 0 ? null : info.tableRow.cells.get(info.column - 1)),
                            trackedOffset.isAfterSpaceEdit(), trackedOffset.isAfterDelete()
                    )
            );
            return true;
        } else if (info.isBeforeCells()) {
            // in the before span
            // we will add it as inside the cell???? since we don't have a before span
            info.tableRow.setBeforeOffset(offset);
            return true;
        } else if (info.isInCellSpan()) {
            // in the after span
            info.tableRow.cells.set(info.column, info.tableCell.withSpanTrackedOffset(offset - info.tableCell.getInsideEndOffset()));
            return true;
        } else if (info.isAfterCells()) {
            // must be after the row, can go after the row.
            info.tableRow.setAfterOffset(offset);
            return true;
        }

        return false;
    }

    /*
        Table Manipulation Helper API
    */
    public List<TableRow> getAllRows() {
        return getAllSectionsRows(ALL_TABLE_ROWS);
    }

    public List<TableRow> getAllContentRows() {
        return getAllSectionsRows(ALL_CONTENT_ROWS);
    }

    public List<TableRow> getAllSectionRows() {
        return getAllSectionsRows(ALL_SECTIONS);
    }

    private List<TableRow> getAllSectionsRows(TableSection... sections) {
        ArrayList<TableRow> rows = new ArrayList<>(header.rows.size() + body.rows.size());
        for (TableSection section : sections) {
            rows.addAll(section.rows);
        }
        return rows;
    }

    public boolean isAllRowsSeparator(int index) {
        return index >= header.rows.size() && index < header.rows.size() + separator.rows.size();
    }

    public TableSection getAllRowsSection(int index) {
        for (TableSection section : ALL_SECTIONS) {
            if (index < section.rows.size()) return section;
            index -= section.rows.size();
        }
        return null;
    }

    public int getAllRowsCount() {
        return header.rows.size() + separator.rows.size() + body.rows.size();
    }

    public int getAllContentRowsCount() {
        return header.rows.size() + body.rows.size();
    }

    public int getAllSectionsRowsCount() {
        return header.rows.size() + separator.rows.size() + body.rows.size() + caption.rows.size();
    }

    public void forAllRows(TableRowManipulator manipulator) {
        forAllSectionsRows(0, Integer.MAX_VALUE, ALL_TABLE_ROWS, manipulator);
    }

    public void forAllRows(int startIndex, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, Integer.MAX_VALUE, ALL_TABLE_ROWS, manipulator);
    }

    public void forAllRows(int startIndex, int count, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, count, ALL_TABLE_ROWS, manipulator);
    }

    public void forAllContentRows(TableRowManipulator manipulator) {
        forAllSectionsRows(0, Integer.MAX_VALUE, ALL_CONTENT_ROWS, manipulator);
    }

    public void forAllContentRows(int startIndex, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, Integer.MAX_VALUE, ALL_CONTENT_ROWS, manipulator);
    }

    public void forAllContentRows(int startIndex, int count, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, count, ALL_CONTENT_ROWS, manipulator);
    }

    public void forAllSectionRows(TableRowManipulator manipulator) {
        forAllSectionsRows(0, Integer.MAX_VALUE, ALL_SECTIONS, manipulator);
    }

    public void forAllSectionRows(int startIndex, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, Integer.MAX_VALUE, ALL_SECTIONS, manipulator);
    }

    public void forAllSectionRows(int startIndex, int count, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, count, ALL_SECTIONS, manipulator);
    }

    public void forAllHeaderRows(TableRowManipulator manipulator) {
        forAllSectionsRows(0, Integer.MAX_VALUE, ALL_HEADER_ROWS, manipulator);
    }

    public void forAllHeaderRows(int startIndex, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, Integer.MAX_VALUE, ALL_HEADER_ROWS, manipulator);
    }

    public void forAllHeaderRows(int startIndex, int count, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, count, ALL_HEADER_ROWS, manipulator);
    }

    public void forAllBodyRows(TableRowManipulator manipulator) {
        forAllSectionsRows(0, Integer.MAX_VALUE, ALL_BODY_ROWS, manipulator);
    }

    public void forAllBodyRows(int startIndex, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, Integer.MAX_VALUE, ALL_HEADER_ROWS, manipulator);
    }

    public void forAllBodyRows(int startIndex, int count, TableRowManipulator manipulator) {
        forAllSectionsRows(startIndex, count, ALL_HEADER_ROWS, manipulator);
    }

    public void deleteRows(int rowIndex, int count) {
        if (rowIndex <= header.rows.size()) {
            int i = count;
            while (i-- > 0 && rowIndex < header.rows.size()) {
                header.rows.remove(rowIndex);
            }
        } else if (rowIndex >= header.rows.size() + separator.rows.size()) {
            int index = rowIndex - header.rows.size() - separator.rows.size();
            int i = count;
            while (i-- > 0 && index < body.rows.size()) {
                body.rows.remove(index);
            }
        }
    }

    public void insertRows(int rowIndex, int count) {
        int maxColumns = getMaxColumns();
        if (rowIndex <= header.rows.size()) {
            insertRows(header.rows, rowIndex, count, maxColumns);
        } else {
            insertRows(body.rows, rangeLimit(rowIndex - header.rows.size() - separator.rows.size(), 0, body.rows.size()), count, maxColumns);
        }
    }

    private void insertRows(
            ArrayList<TableRow> rows,
            int index,
            int count,
            int maxColumns
    ) {
        int i = count;
        while (i-- > 0) {
            TableRow emptyRow = new TableRow();
            emptyRow.appendColumns(maxColumns);
            if (index >= rows.size()) {
                rows.add(emptyRow);
            } else {
                rows.add(index, emptyRow);
            }
        }
    }

    public void insertColumns(int column, int count) {
        forAllContentRows((row, allRowsIndex, rows, index) -> {
            rows.get(index).insertColumns(column, count);
            return 0;
        });

        // insert separator columns separately, since they are not reduced in finalize
        for (TableRow row : separator.rows) {
            row.insertColumns(column, count);
        }
    }

    public void deleteColumns(int column, int count) {
        forAllContentRows((row, allRowsIndex, rows, index) -> {
            rows.get(index).deleteColumns(column, count);
            return 0;
        });

        // delete separator columns separately, since they are not reduced in finalize
        for (TableRow row : separator.rows) {
            row.deleteColumns(column, count);
        }
    }

    public void moveColumn(int fromColumn, int toColumn) {
        forAllContentRows((row, allRowsIndex, rows, index) -> {
            rows.get(index).moveColumn(fromColumn, toColumn);
            return 0;
        });

        // move separator columns separately, since they are not reduced in finalize
        for (TableRow row : separator.rows) {
            row.moveColumn(fromColumn, toColumn);
        }
    }

    /**
     * Test all rows for having given column empty. All columns after row's max column are
     * empty
     *
     * @param column index in allRows list
     * @return true if column is empty for all rows, separator row excluded
     */
    public boolean isEmptyColumn(int column) {
        boolean[] result = new boolean[] { true };
        forAllContentRows((row, allRowsIndex, rows, index) -> {
            if (!row.isEmptyColumn(column)) {
                result[0] = false;
                return TableRowManipulator.BREAK;
            }
            return 0;
        });

        return result[0];
    }

    /**
     * Test a row for having all empty columns
     *
     * @param rowIndex index in allRows list
     * @return true if row is empty or is a separator row
     */
    public boolean isAllRowsEmptyAt(int rowIndex) {
        return isEmptyRowAt(rowIndex, ALL_TABLE_ROWS);
    }

    /**
     * Test a row for having all empty columns
     *
     * @param rowIndex index in allRows list
     * @return true if row is empty or is a separator row
     */
    public boolean isContentRowsEmptyAt(int rowIndex) {
        return isEmptyRowAt(rowIndex, ALL_CONTENT_ROWS);
    }

    /**
     * Test a row for having all empty columns
     *
     * @param rowIndex index in allRows list
     * @param sections sections to use for rows array generation
     * @return true if row is empty or is a separator row
     */
    private boolean isEmptyRowAt(int rowIndex, TableSection[] sections) {
        boolean[] result = new boolean[] { false };
        forAllSectionsRows(rowIndex, 1, sections, (row, allRowsIndex, rows, index) -> {
            if (row.isEmpty()) {
                result[0] = true;
            }
            return TableRowManipulator.BREAK;
        });

        return result[0];
    }

    /*
     * Used during table construction by building the table
     * as the AST visiting process (Flexmark or HTML)
     *
     */

    public boolean getHeader() {
        return isHeading;
    }

    public void setHeader(boolean header) {
        isHeading = header;
    }

    public boolean isSeparator() {
        return isSeparator;
    }

    public void setSeparator(boolean separator) {
        isSeparator = separator;
    }

    public void setHeader() {
        isHeading = true;
        isSeparator = false;
    }

    public void setSeparator() {
        isSeparator = true;
        isHeading = false;
    }

    public void setBody() {
        isSeparator = false;
        isHeading = false;
    }

    public void nextRow() {
        if (isSeparator) throw new IllegalStateException("Only one separator row allowed");
        if (isHeading) {
            header.nextRow();
        } else {
            body.nextRow();
        }
    }

    /**
     * @param cell cell to add
     */
    public void addCell(@NotNull TableCell cell) {
        TableSection tableSection = isSeparator ? separator : isHeading ? header : body;

        if (isSeparator && (cell.columnSpan != 1 || cell.rowSpan != 1)) { throw new IllegalStateException("Separator columns cannot span rows/columns"); }

        TableRow currentRow = tableSection.get(tableSection.row);

        // skip cells that are already set
        while (tableSection.column < currentRow.cells.size() && currentRow.cells.get(tableSection.column) != null) { tableSection.column++; }

        int rowSpan = 0;
        while (rowSpan < cell.rowSpan) {
            tableSection.get(tableSection.row + rowSpan).set(tableSection.column, cell);

            // set the rest to NULL cells up to null column
            int columnSpan = 1;
            while (columnSpan < cell.columnSpan) {
                tableSection.expandTo(tableSection.row + rowSpan, tableSection.column + columnSpan);
                if (tableSection.get(tableSection.row + rowSpan).cells.get(tableSection.column + columnSpan) != null) { break; }

                tableSection.rows.get(tableSection.row + rowSpan).set(tableSection.column + columnSpan, TableCell.NULL);
                columnSpan++;
            }
            rowSpan++;
        }

        tableSection.column += cell.columnSpan;
    }

    public void normalize() {
        header.normalize();
        separator.normalize();
        body.normalize();
    }

    public void finalizeTable() {
        // remove null cells
        normalize();

        if (options.fillMissingColumns) {
            fillMissingColumns(options.formatTableFillMissingMinColumn);
        }

        int sepColumns = getMaxColumns();
        alignments = new CellAlignment[sepColumns];
        columnWidths = new int[sepColumns];
        BitSet spanAlignment = new BitSet(sepColumns);
        List<ColumnSpan> columnSpans = new ArrayList<>();
        Ref<Integer> delta = new Ref<>(0);

        if (separator.rows.size() > 0) {
            TableRow row = separator.rows.get(0);
            int j = 0;
            int jSpan = 0;
            delta.value = 0;
            for (TableCell cell : row.cells) {
                // set alignment if not already set or was set by a span and this column is not a span
                if ((alignments[jSpan] == null || cell.columnSpan == 1 && spanAlignment.get(jSpan)) && cell.alignment != CellAlignment.NONE) {
                    alignments[jSpan] = cell.alignment;
                    if (cell.columnSpan > 1) spanAlignment.set(jSpan);
                }
                j++;
                jSpan += cell.columnSpan;
            }
        }

        if (header.rows.size() > 0) {
            int i = 0;
            for (TableRow row : header.rows) {
                int j = 0;
                int jSpan = 0;
                int kMax = row.cells.size();
                for (int k = 0; k < kMax; k++) {
                    TableCell cell = row.cells.get(k);

                    // set alignment if not already set or was set by a span and this column is not a span
                    if ((alignments[jSpan] == null || cell.columnSpan == 1 && spanAlignment.get(jSpan)) && cell.alignment != CellAlignment.NONE) {
                        alignments[jSpan] = cell.alignment;
                        if (cell.columnSpan > 1) spanAlignment.set(jSpan);
                    }

                    delta.value = 0;
                    BasedSequence cellText = cellText(row.cells, k, false, true, 0, null, delta);
                    int width = options.charWidthProvider.getStringWidth(cellText) + options.spacePad + options.pipeWidth * cell.columnSpan;
                    if (cell.columnSpan > 1) {
                        columnSpans.add(new ColumnSpan(j, cell.columnSpan, width));
                    } else {
                        if (columnWidths[jSpan] < width) columnWidths[jSpan] = width;
                    }

                    j++;
                    jSpan += cell.columnSpan;
                }
                i++;
            }
        }

        if (body.rows.size() > 0) {
            int i = 0;
            for (TableRow row : body.rows) {
                int j = 0;
                int jSpan = 0;
                int kMax = row.cells.size();
                for (int k = 0; k < kMax; k++) {
                    TableCell cell = row.cells.get(k);
                    delta.value = 0;
                    BasedSequence cellText = cellText(row.cells, k, false, false, 0, null, delta);
                    int width = options.charWidthProvider.getStringWidth(cellText) + options.spacePad + options.pipeWidth * cell.columnSpan;
                    if (cell.columnSpan > 1) {
                        columnSpans.add(new ColumnSpan(jSpan, cell.columnSpan, width));
                    } else {
                        if (columnWidths[jSpan] < width) columnWidths[jSpan] = width;
                    }

                    j++;
                    jSpan += cell.columnSpan;
                }
                i++;
            }
        }

        // add separator column widths to the calculation
        if (separator.rows.size() == 0 || body.rows.size() > 0 || header.rows.size() > 0) {
            int j = 0;
            delta.value = 0;
            for (CellAlignment alignment : alignments) {
                CellAlignment alignment1 = adjustCellAlignment(alignment);
                int colonCount = alignment1 == CellAlignment.LEFT || alignment1 == CellAlignment.RIGHT ? 1 : alignment1 == CellAlignment.CENTER ? 2 : 0;
                int dashCount = 0;
                int dashesOnly = Utils.minLimit(dashCount, options.minSeparatorColumnWidth - colonCount, options.minSeparatorDashes);
                if (dashCount < dashesOnly) dashCount = dashesOnly;
                int width = dashCount * options.dashWidth + colonCount * options.colonWidth + options.pipeWidth;
                if (columnWidths[j] < width) columnWidths[j] = width;
                j++;
            }
        } else {
            // keep as is
            int j = 0;
            delta.value = 0;
            for (TableCell cell : separator.rows.get(0).cells) {
                CellAlignment alignment = adjustCellAlignment(cell.alignment);
                int colonCount = alignment == CellAlignment.LEFT || alignment == CellAlignment.RIGHT ? 1 : alignment == CellAlignment.CENTER ? 2 : 0;
                BasedSequence trim = cell.text.trim(COLON_TRIM_CHARS);
                int dashCount = trim.length();
                int dashesOnly = Utils.minLimit(dashCount, options.minSeparatorColumnWidth - colonCount, options.minSeparatorDashes);
                if (dashCount < dashesOnly) dashCount = dashesOnly;
                int width = dashCount * options.dashWidth + colonCount * options.colonWidth + options.pipeWidth;
                if (columnWidths[j] < width) columnWidths[j] = width;
                j++;
            }
        }

        if (!columnSpans.isEmpty()) {
            // now need to distribute extra width from spans to contained columns
            BitSet unfixedColumns = new BitSet(sepColumns);
            List<ColumnSpan> newColumnSpans = new ArrayList<>(columnSpans.size());

            for (ColumnSpan columnSpan : columnSpans) {
                int spanWidth = spanWidth(columnSpan.startColumn, columnSpan.columnSpan);
                if (spanWidth < columnSpan.width) {
                    // not all fits, need to distribute the remainder
                    unfixedColumns.set(columnSpan.startColumn, columnSpan.startColumn + columnSpan.columnSpan);
                    newColumnSpans.add(columnSpan);
                }
            }

            // we now distribute additional width equally between columns that are spanned to unfixed columns
            while (!newColumnSpans.isEmpty()) {
                columnSpans = newColumnSpans;

                BitSet fixedColumns = new BitSet(sepColumns);
                newColumnSpans.clear();

                // remove spans that already fit into fixed columns
                for (ColumnSpan columnSpan : columnSpans) {
                    int spanWidth = spanWidth(columnSpan.startColumn, columnSpan.columnSpan);
                    int fixedWidth = spanFixedWidth(unfixedColumns, columnSpan.startColumn, columnSpan.columnSpan);

                    if (spanWidth <= fixedWidth) {
                        fixedColumns.set(columnSpan.startColumn, columnSpan.startColumn + columnSpan.columnSpan);
                    } else {
                        newColumnSpans.add(columnSpan);
                    }
                }

                // reset fixed columns
                unfixedColumns.andNot(fixedColumns);
                columnSpans = newColumnSpans;
                newColumnSpans.clear();

                for (ColumnSpan columnSpan : columnSpans) {
                    int spanWidth = spanWidth(columnSpan.startColumn, columnSpan.columnSpan);
                    int fixedWidth = spanFixedWidth(unfixedColumns, columnSpan.startColumn, columnSpan.columnSpan);

                    if (spanWidth > fixedWidth) {
                        // not all fits, need to distribute the remainder to unfixed columns
                        int distributeWidth = spanWidth - fixedWidth;
                        int unfixedColumnCount = unfixedColumns.get(columnSpan.startColumn, columnSpan.startColumn + columnSpan.columnSpan).cardinality();
                        int perSpanWidth = distributeWidth / unfixedColumnCount;
                        int extraWidth = distributeWidth - perSpanWidth * unfixedColumnCount;

                        for (int i = 0; i < columnSpan.columnSpan; i++) {
                            if (unfixedColumns.get(columnSpan.startColumn + i)) {
                                columnWidths[columnSpan.startColumn + i] += perSpanWidth;
                                if (extraWidth > 0) {
                                    columnWidths[columnSpan.startColumn + i]++;
                                    extraWidth--;
                                }
                            }
                        }
                        newColumnSpans.add(columnSpan);
                    }
                }
            }
        }
    }

    public void fillMissingColumns() {
        fillMissingColumns(null);
    }

    public void fillMissingColumns(Integer minColumn) {
        int minColumns = getMinColumns();
        int maxColumns = getMaxColumns();
        if (minColumns < maxColumns) {
            // add empty cells to rows that have less
            for (TableRow row : header.rows) {
                row.fillMissingColumns(minColumn, maxColumns);
            }

            for (TableRow row : body.rows) {
                row.fillMissingColumns(minColumn, maxColumns);
            }
        }
    }

    private boolean setTrackedOffsetIndex(int offset, int index) {
        TrackedOffset trackedOffset = findTrackedOffset(offset);
        if (trackedOffset != null) {
            trackedOffset.setIndex(index);
            return true;
        }

        // QUERY: Triggered after sort table
        // RELEASE : remove exception throwing
        //throw new IllegalStateException(String.format("offset: %d not in tracked offsets: %s", offset, trackedOffsets));
        return false;
    }

    /**
     * Transpose table
     *
     * @param columnHeaders number of first columns to use as header rows, 0..maxColumns
     * @return transposed table
     */
    public MarkdownTable transposed(int columnHeaders) {
        MarkdownTable transposed = new MarkdownTable(tableChars, options);
        transposed.trackedOffsets.addAll(trackedOffsets);

        int maxRows = getAllRowsCount() - 1; // don't count separator rows
        int maxCols = getMaxColumns();
        TableCell[][] tableCells = new TableCell[maxRows][];
        for (int i = 0; i < maxRows; i++) {
            tableCells[i] = new TableCell[maxCols];
        }

        // get a matrix of all cells
        forAllSectionsRows(0, Integer.MAX_VALUE, ALL_CONTENT_ROWS, (row, allRowsIndex, sectionRows, sectionRowIndex) -> {
            TableCell[] tableCellRow = tableCells[allRowsIndex];
            int iMax = row.cells.size();
            int col = 0;
            for (int i = 0; i < iMax; i++) {
                TableCell cell = row.cells.get(i);
                for (int span = 0; span < cell.columnSpan; span++) {
                    tableCellRow[col++] = new TableCell(cell, span == 0, 1, 1, null);
                }
            }
            return 0;
        });

        transposed.setHeader();
        int colHdrs = Math.min(Math.max(0, columnHeaders), maxCols);
        for (int c = 0; c < colHdrs; c++) {
            for (int r = 0; r < maxRows; r++) {
                TableCell cell = tableCells[r][c];
                transposed.addCell(cell == null ? DEFAULT_CELL : cell);
            }
            transposed.nextRow();
        }

        TableRow sepRow = separator.rows.get(0);
        transposed.setSeparator();
        int iMax = sepRow.cells.size();
        for (int i = 0; i < maxRows; i++) {
            if (i < iMax) {
                transposed.addCell(new TableCell(sepRow.cells.get(i), true, 1, 1, null));
            } else {
                transposed.addCell(new TableCell("---", 1, 1));
            }
        }

        transposed.setBody();
        for (int c = colHdrs; c < maxCols; c++) {
            for (int r = 0; r < maxRows; r++) {
                TableCell cell = tableCells[r][c];
                transposed.addCell(cell == null ? DEFAULT_CELL : cell);
            }
            transposed.nextRow();
        }

        transposed.setCaptionCell(getCaptionCell());

        return transposed;
    }

    /**
     * Sort table
     *
     * @param columnSorts         column sort information
     * @param textCollectionFlags collection flags to use for collecting cell text
     * @param numericSuffixTester predicate to test non-numeric suffix of numeric column content, return true if suffix is acceptable, null will result in all suffixes being accepted
     * @return sorted table
     */
    public MarkdownTable sorted(ColumnSort[] columnSorts, int textCollectionFlags, @Nullable NumericSuffixPredicate numericSuffixTester) {
        MarkdownTable sorted = new MarkdownTable(tableChars, options);
        sorted.trackedOffsets.addAll(trackedOffsets);

        sorted.setHeader();
        forAllSectionsRows(0, Integer.MAX_VALUE, ALL_HEADER_ROWS, (row, allRowsIndex, sectionRows, sectionRowIndex) -> {
            int iMax = row.cells.size();
            for (int i = 0; i < iMax; i++) {
                TableCell cell = row.cells.get(i);
                sorted.addCell(cell == DEFAULT_CELL ? cell : new TableCell(cell, true, cell.rowSpan, cell.columnSpan, cell.alignment));
            }
            sorted.nextRow();
            return 0;
        });

        sorted.setSeparator();
        TableRow sepRow = separator.rows.get(0);
        int iMax = sepRow.cells.size();
        CellAlignment[] alignments = new CellAlignment[iMax];

        for (int i = 0; i < iMax; i++) {
            TableCell cell = sepRow.cells.get(i);
            sorted.addCell(cell == DEFAULT_CELL ? cell : new TableCell(cell, true, cell.rowSpan, cell.columnSpan, cell.alignment));
            alignments[i] = cell.alignment;
        }

        List<TableRow> rows = getAllSectionsRows(body);
        int[] cellSizes = new int[iMax];

        int rMax = rows.size();
        int cMax = getMaxBodyColumns();

        for (int r = 0; r < rMax; r++) {
            for (ColumnSort columnSort : columnSorts) {
                int c = columnSort.column;
                if (c >= 0 && c < cMax) {
                    IndexSpanOffset spanIndex = rows.get(r)
                            .indexOf(c);
                    TableCell cell = rows.get(r).cells
                            .get(spanIndex.index);
                    if (spanIndex.index == c && cell != null) {
                        // not in span
                        cellSizes[c] = Math.max(cellSizes[c], cell.text.length());
                    }
                }
            }
        }

        TextCollectingVisitor visitor = new TextCollectingVisitor();

        NumericSuffixPredicate numericSuffixPredicate = numericSuffixTester == null ? ALL_SUFFIXES_SORT : numericSuffixTester;
        Comparator<TableRow> rowComparator = (o1, o2) -> {
            for (ColumnSort columnSort : columnSorts) {
                int c = columnSort.column;
                if (c >= 0 && c < cMax) {
                    int cellSize = cellSizes[c];
                    if (cellSize > 0) {
                        // sorting on this column
                        Sort sort = columnSort.sort;
                        boolean descending = sort.isDescending();
                        boolean numeric = sort.isNumeric();
                        boolean numericLast = sort.isNumericLast();

                        IndexSpanOffset spanIndex1 = o1.indexOf(c);
                        TableCell cell1 = o1.cells.get(spanIndex1.index);

                        IndexSpanOffset spanIndex2 = o2.indexOf(c);
                        TableCell cell2 = o2.cells.get(spanIndex2.index);
                        int result;

                        if (spanIndex1.index == c && cell1 != null && spanIndex2.index == c && cell2 != null) {
                            // not in span, compare them by padding and aligning then comparing strings
                            int padLeft1 = 0;
                            int padLeft2 = 0;
                            int padRight1 = 0;
                            int padRight2 = 0;
                            String cellText1 = cell1.tableCellNode == null ? cell1.text.toString() : visitor.collectAndGetText(cell1.tableCellNode, textCollectionFlags).trim();
                            String cellText2 = cell2.tableCellNode == null ? cell2.text.toString() : visitor.collectAndGetText(cell2.tableCellNode, textCollectionFlags).trim();
                            int diff1 = cellSize - cellText1.length();
                            int diff2 = cellSize - cellText2.length();

                            switch (alignments[c]) {
                                case CENTER:
                                    padLeft1 = diff1 >> 1;
                                    padRight1 = cellSize - padLeft1;
                                    padLeft2 = diff2 >> 1;
                                    padRight2 = cellSize - padLeft2;
                                    break;

                                case RIGHT:
                                    padLeft1 = diff1;
                                    padLeft2 = diff2;
                                    break;

                                default:
                                    break;
                            }

                            if (numeric) {
                                Pair<Number, String> cellNumeric1 = SequenceUtils.parseNumberPrefixOrNull(cellText1, numericSuffixPredicate);
                                Pair<Number, String> cellNumeric2 = SequenceUtils.parseNumberPrefixOrNull(cellText2, numericSuffixPredicate);

                                if (cellNumeric1 != null && cellNumeric2 != null) {
                                    result = compare(cellNumeric1.getFirst(), cellNumeric2.getFirst());
                                    String numericSuffix1 = cellNumeric1.getSecond();
                                    String numericSuffix2 = cellNumeric2.getSecond();

                                    if (result == 0 && (numericSuffixPredicate.sortSuffix(numericSuffix1) || numericSuffixPredicate.sortSuffix(numericSuffix2))) {
                                        if (!numericSuffix1.isEmpty() && numericSuffixPredicate.sortSuffix(numericSuffix1) && !numericSuffix2.isEmpty() && numericSuffixPredicate.sortSuffix(numericSuffix2)) {
                                            result = numericSuffix1.compareTo(cellNumeric2.getSecond());
                                        } else if (!numericSuffix1.isEmpty() && numericSuffixPredicate.sortSuffix(numericSuffix1)) {
                                            result = numericLast ? -1 : 1;
                                            descending = false;
                                        } else if (!numericSuffix2.isEmpty() && numericSuffixPredicate.sortSuffix(numericSuffix2)) {
                                            result = numericLast ? 1 : -1;
                                            descending = false;
                                        }
                                    }
                                } else if (cellNumeric1 != null) {
                                    result = numericLast ? 1 : -1;
                                    descending = false; // do not reverse, numbers first/last are not inverted
                                } else if (cellNumeric2 != null) {
                                    result = numericLast ? -1 : 1;
                                    descending = false; // do not reverse, numbers first/last are not inverted
                                } else {
                                    // compare as text
                                    String cell1Text = RepeatedSequence.ofSpaces(padLeft1).toString() + cellText1 + RepeatedSequence.ofSpaces(padRight1);
                                    String cell2Text = RepeatedSequence.ofSpaces(padLeft2).toString() + cellText2 + RepeatedSequence.ofSpaces(padRight2);
                                    result = cell1Text.compareTo(cell2Text);
                                }
                            } else {
                                String cell1Text = RepeatedSequence.ofSpaces(padLeft1).toString() + cellText1 + RepeatedSequence.ofSpaces(padRight1);
                                String cell2Text = RepeatedSequence.ofSpaces(padLeft2).toString() + cellText2 + RepeatedSequence.ofSpaces(padRight2);
                                result = cell1Text.compareTo(cell2Text);
                            }
                        } else if (spanIndex1.index == c && cell1 != null) {
                            // second in spanned column, so it is first
                            result = 1;
                        } else if (spanIndex2.index == c && cell2 != null) {
                            // first in spanned column, so it is first
                            result = -1;
                        } else {
                            result = 0;
                        }

                        if (result != 0) {
                            return descending ? -result : result;
                        }
                    }
                }
            }
            return 0;
        };

        rows.sort(rowComparator);

        sorted.setBody();
        rMax = rows.size();
        for (int r = 0; r < rMax; r++) {
            TableRow row = rows.get(r);
            iMax = row.cells.size();
            for (int i = 0; i < iMax; i++) {
                TableCell cell = row.cells.get(i);
                sorted.addCell(cell == DEFAULT_CELL ? cell : new TableCell(cell, true, cell.rowSpan, cell.columnSpan, cell.alignment));
            }
            sorted.nextRow();
        }

        sorted.setCaptionCell(getCaptionCell());
        return sorted;
    }

    int appendDashes(LineAppendable out, int dashCount, BasedSequence sepDashes, int dashOffset) {
        int sepDashesLength = sepDashes.length();
        int remainingDashes = Math.max(0, sepDashesLength - dashOffset);

        if (remainingDashes >= dashCount) {
            out.append(sepDashes.subSequence(dashOffset, dashOffset + dashCount));
            remainingDashes -= dashCount;
        } else {
            int usedUpDashes = 0;
            if (remainingDashes > 1) {
                out.append(sepDashes.subSequence(dashOffset, dashOffset + 1));
                remainingDashes--;
                usedUpDashes++;
            }

            out.append('-', dashCount - Math.max(0, remainingDashes + usedUpDashes));

            if (remainingDashes > 0) {
                out.append(sepDashes.subSequence(dashOffset, dashOffset + remainingDashes));
                remainingDashes = 0;
            }
        }
        return sepDashesLength - remainingDashes;
    }

    public void appendTable(LineAppendable out) {
        // we will prepare the separator based on max columns
        CharSequence linePrefix = formatTableIndentPrefix;

        trackedOffsets.sort(Comparator.comparing(TrackedOffset::getOffset));

        out.pushOptions();
        out.removeOptions(LineAppendable.F_WHITESPACE_REMOVAL);

        finalizeTable();

        appendRows(out, header.rows, true, linePrefix);

        {
            out.append(linePrefix);

            TableRow row = separator.rows.size() > 0 ? separator.rows.get(0) : null;

            if (row != null && row.beforeOffset != NOT_TRACKED) {
                setTrackedOffsetIndex(row.beforeOffset, out.offsetWithPending());
            }

            int j = 0;
            Ref<Integer> delta = new Ref<>(0);
            for (CellAlignment alignment : alignments) {
                CellAlignment alignment1 = adjustCellAlignment(alignment);

                int colonCount = alignment1 == CellAlignment.LEFT || alignment1 == CellAlignment.RIGHT ? 1 : alignment1 == CellAlignment.CENTER ? 2 : 0;
                int diff = columnWidths[j] - colonCount * options.colonWidth - options.pipeWidth;
                int dashCount = (delta.value + diff) / options.dashWidth;
                int dashesOnly = Utils.minLimit(dashCount, options.minSeparatorColumnWidth - colonCount, options.minSeparatorDashes);
                if (dashCount < dashesOnly) dashCount = dashesOnly;

                if (Math.abs(delta.value + diff - (dashCount + 1) * options.dashWidth) < Math.abs(delta.value + diff - dashCount * options.dashWidth)) {
                    dashCount++;
                }

                delta.value += diff - dashCount * options.dashWidth;

                int trackedPos;
                TableCell cell = null;
                TableCell previousCell = null;

                if (row != null) {
                    List<TableCell> cells = row.cells;
                    if (j < cells.size()) {
                        cell = cells.get(j);
                        if (j > 0) previousCell = cells.get(j - 1);
                    }
                }

                trackedPos = cell == null ? NOT_TRACKED : minLimit(cell.trackedTextOffset, 0);
                BasedSequence sepText = cell == null ? BasedSequence.NULL : cell.text.trim(COLON_TRIM_CHARS);
                int sepDashOffset = 0;

                if (trackedPos != NOT_TRACKED) {
                    if (options.leadTrailPipes && j == 0) out.append('|');
                    boolean beforeFirstColon = trackedPos == 0 && cell.text.charAt(trackedPos) == ':';
                    boolean afterFirstColon = trackedPos == 1 && cell.text.charAt(trackedPos - 1) == ':';
                    boolean beforeLastColon = trackedPos == cell.text.length() - 1 && cell.text.charAt(trackedPos) == ':';
                    boolean afterLastColon = trackedPos == cell.text.length() && cell.text.charAt(trackedPos - 1) == ':';
                    boolean afterLastDash = trackedPos == cell.text.length() && cell.text.charAt(trackedPos - 1) == '-';

                    if (alignment1 == CellAlignment.LEFT || alignment1 == CellAlignment.CENTER) {
                        if (beforeFirstColon) {
                            setTrackedOffsetIndex(cell.trackedTextOffset + cell.getInsideStartOffset(previousCell), out.offsetWithPending());
                            trackedPos = NOT_TRACKED;
                            out.append(':');
                        } else if (afterFirstColon) {
                            out.append(':');
                            setTrackedOffsetIndex(cell.trackedTextOffset + cell.getInsideStartOffset(previousCell), out.offsetWithPending());
                            trackedPos = NOT_TRACKED;
                        } else {
                            out.append(':');
                        }
                    } else {
                        beforeFirstColon = false;
                        afterFirstColon = false;
                    }

                    if (!afterFirstColon && !beforeFirstColon && !afterLastColon && !beforeLastColon) {
                        if (trackedPos == 0) {
                            setTrackedOffsetIndex(cell.trackedTextOffset + cell.getInsideStartOffset(previousCell), out.offsetWithPending());
                            trackedPos = NOT_TRACKED;
                            sepDashOffset = appendDashes(out, dashCount, sepText, sepDashOffset);
                        } else if (!afterLastDash && trackedPos < dashCount) {
                            sepDashOffset = appendDashes(out, trackedPos, sepText, sepDashOffset);
                            setTrackedOffsetIndex(cell.trackedTextOffset + cell.getInsideStartOffset(previousCell), out.offsetWithPending());
                            sepDashOffset = appendDashes(out, dashCount - trackedPos, sepText, sepDashOffset);
                            trackedPos = NOT_TRACKED;
                        } else {
                            sepDashOffset = appendDashes(out, dashCount, sepText, sepDashOffset);
                            setTrackedOffsetIndex(cell.trackedTextOffset + cell.getInsideStartOffset(previousCell), out.offsetWithPending());
                            trackedPos = NOT_TRACKED;
                        }
                    } else {
                        sepDashOffset = appendDashes(out, dashCount, sepText, sepDashOffset);
                    }

                    if (alignment1 == CellAlignment.RIGHT || alignment1 == CellAlignment.CENTER) {
                        if (afterLastColon) {
                            out.append(':');
                            setTrackedOffsetIndex(cell.trackedTextOffset + cell.getInsideStartOffset(previousCell), out.offsetWithPending());
                            trackedPos = NOT_TRACKED;
                        } else if (beforeLastColon) {
                            setTrackedOffsetIndex(cell.trackedTextOffset + cell.getInsideStartOffset(previousCell), out.offsetWithPending());
                            trackedPos = NOT_TRACKED;
                            out.append(':');
                        } else {
                            out.append(':');
                        }
                    } else if (afterLastColon || beforeLastColon) {
                        setTrackedOffsetIndex(cell.trackedTextOffset + cell.getInsideStartOffset(previousCell), out.offsetWithPending());
                        trackedPos = NOT_TRACKED;
                    }

                    assert trackedPos == NOT_TRACKED;
                } else {
                    if (options.leadTrailPipes && j == 0) out.append('|');
                    if (alignment1 == CellAlignment.LEFT || alignment1 == CellAlignment.CENTER) { out.append(':'); }

                    sepDashOffset = appendDashes(out, dashCount, sepText, sepDashOffset);

                    if (alignment1 == CellAlignment.RIGHT || alignment1 == CellAlignment.CENTER) { out.append(':'); }
                }

                j++;
                if (options.leadTrailPipes || j < alignments.length) out.append('|');
            }

            if (row != null && row.afterOffset != NOT_TRACKED) {
                setTrackedOffsetIndex(row.afterOffset, out.offsetWithPending());
            }

            out.line();
        }

        appendRows(out, body.rows, false, linePrefix);

        TableCell captionCell = getCaptionCell();
        String captionText = formattedCaption(captionCell.text, options);
        if (captionText != null) {
            BasedSequence formattedCaption = BasedSequence.of(captionText).subSequence(0, ((CharSequence) captionText).length());
            boolean handled = false;

            if (this.caption.rows.size() > 0) {
                TableRow row = this.caption.rows.get(0);
                if (captionCell.trackedTextOffset != NOT_TRACKED || row.beforeOffset != NOT_TRACKED || row.afterOffset != NOT_TRACKED) {
                    TableCell cell = captionCell;

                    out.line();

                    if (row.beforeOffset != NOT_TRACKED) {
                        setTrackedOffsetIndex(row.beforeOffset, out.offsetWithPending());
                    }

                    captionCell = captionCell.withText(captionCell.text.trim());
                    if (cell.trackedTextOffset != NOT_TRACKED) {
                        captionCell = captionCell.withTrackedOffset(minLimit(cell.trackedTextOffset - cell.text.trimmedStart().length(), 0));
                    }

                    boolean addOpenCaptionSpace = false;
                    boolean addCloseCaptionSpace = false;

                    if (!captionCell.text.isBlank()) {
                        switch (options.formatTableCaptionSpaces) {
                            case ADD:
                                addOpenCaptionSpace = true;
                                addCloseCaptionSpace = true;
                                break;

                            case REMOVE:
                                break;

                            default:
                            case AS_IS:
                                addOpenCaptionSpace = cell.text.startsWith(" ");
                                addCloseCaptionSpace = cell.text.endsWith(" ");
                                break;
                        }
                    }

                    out.append(linePrefix);
                    out.append('[');
                    if (addOpenCaptionSpace) out.append(' ');

                    int cellOffset = out.offsetWithPending();

                    row.cells.set(0, captionCell);
                    Ref<Integer> delta = new Ref<>(0);
                    BasedSequence cellText = cellText(row.cells, 0, true, false, 0, CellAlignment.LEFT, delta);
                    int captionOffset = out.offsetWithPending();

                    if (row.cells.size() > 0) {
                        if (cell.trackedTextOffset != NOT_TRACKED) {
                            TableCell adjustedCell = row.cells.get(0);
                            if (adjustedCell.trackedTextOffset != NOT_TRACKED) {
                                setTrackedOffsetIndex(cell.trackedTextOffset + cell.text.getStartOffset(), cellOffset + (cellText.isBlank() ? 1 : minLimit(adjustedCell.trackedTextOffset, 0) + adjustedCell.trackedTextAdjust));
                            }
                        }
                        row.cells.set(0, cell);
                    } else {
                        row.cells.add(cell);
                    }

                    out.append(cellText);

                    if (addCloseCaptionSpace) out.append(' ');
                    out.append(']');

                    if (row.afterOffset != NOT_TRACKED) {
                        setTrackedOffsetIndex(row.afterOffset, out.offsetWithPending());
                    }
                    out.line();

                    handled = true;
                }
            }

            if (!handled) {
                out.popOptions().pushOptions();
                out.line().append(linePrefix).append('[').append(formattedCaption).append(']').line();
            }
        }
        out.popOptions();
    }

    public static void appendFormattedCaption(
            LineAppendable out,
            BasedSequence caption,
            TableFormatOptions options
    ) {
        String formattedCaption = formattedCaption(caption, options);
        if (formattedCaption != null) {
            out.line().append('[').append(formattedCaption).append(']').line();
        }
    }

    public static String formattedCaption(
            BasedSequence caption,
            TableFormatOptions options
    ) {
        boolean append = caption.isNotNull();

        switch (options.formatTableCaption) {
            case ADD:
                append = true;
                break;

            case REMOVE_EMPTY:
                append = !(caption.isBlank());
                break;

            case REMOVE:
                append = false;
                break;

            default:
            case AS_IS:
                break;
        }

        if (append) {
            String captionSpaces = "";

            switch (options.formatTableCaptionSpaces) {
                case ADD:
                    captionSpaces = " ";
                    break;

                case REMOVE:
                    break;

                default:
                case AS_IS:
                    break;
            }
            return captionSpaces + caption.toString() + captionSpaces;
        }
        return null;
    }

    private boolean pipeNeedsSpaceBefore(TableCell cell) {
        //if (cell.trackedTextOffset != NO_TRACKED_OFFSET) {
        //    return cell.text.equals(" ") || cell.trackedTextOffset > cell.text.length() || !cell.text.subSequence(cell.trackedTextOffset).endsWith(" ");
        //}
        return cell.text.equals(" ") || !cell.text.endsWith(" ");
    }

    private boolean pipeNeedsSpaceAfter(TableCell cell) {
        return cell.text.equals(" ") || !cell.text.startsWith(" ");
    }

    private void appendRows(
            LineAppendable out,
            List<TableRow> rows,
            boolean isHeader,
            CharSequence linePrefix
    ) {
        for (TableRow row : rows) {
            int j = 0;
            int jSpan = 0;
            Ref<Integer> delta = new Ref<>(0);

            out.append(linePrefix);

            if (row.beforeOffset != NOT_TRACKED) {
                setTrackedOffsetIndex(row.beforeOffset, out.offsetWithPending());
            }

            int iMax = row.cells.size();
            for (int i = 0; i < iMax; i++) {
                TableCell cell = row.cells.get(i);

                if (j == 0) {
                    if (options.leadTrailPipes) {
                        out.append('|');
                        if (options.spaceAroundPipes && pipeNeedsSpaceAfter(cell)) { out.append(' '); }
                    }
                } else {
                    if (options.spaceAroundPipes && pipeNeedsSpaceAfter(cell)) out.append(' ');
                }

                CellAlignment cellAlignment = isHeader && cell.alignment != CellAlignment.NONE ? cell.alignment : alignments[jSpan];

                BasedSequence cellText = cellText(row.cells, i, true, isHeader,
                        spanWidth(jSpan, cell.columnSpan) - options.spacePad - options.pipeWidth * cell.columnSpan,
                        cellAlignment, delta);

                if (cell.trackedTextOffset != NOT_TRACKED) {
                    TableCell adjustedCell = row.cells.get(i);
                    if (adjustedCell.trackedTextOffset != NOT_TRACKED) {
                        int cellOffset = out.offsetWithPending();
                        int adjustForBlank = cell.text.isBlank() ? -1 : 0;
                        if (!setTrackedOffsetIndex(cell.trackedTextOffset + cell.getTextStartOffset(i == 0 ? null : row.cells.get(i - 1)), cellOffset + minLimit(adjustedCell.trackedTextOffset + adjustForBlank, 0) + adjustedCell.trackedTextAdjust)) {
                            // QUERY: Triggered after sort table in MdNav for header row
                            System.out.println(String.format("Offset not found: cell.trackedTextOffset: %d, adjusted trackedOffset: %d in offsets: %s"
                                    , cell.trackedTextOffset
                                    , cell.trackedTextOffset + cell.getTextStartOffset(i == 0 ? null : row.cells.get(i - 1))
                                    , trackedOffsets
                            ));
                        }
                    }
                }

                out.append(cellText);

                j++;
                jSpan += cell.columnSpan;

                if (j < alignments.length) {
                    if (options.spaceAroundPipes && pipeNeedsSpaceBefore(cell)) out.append(' ');
                    appendColumnSpan(out, cell.columnSpan, cell.getInsideEndOffset(), cell.spanTrackedOffset);
                } else if (options.leadTrailPipes) {
                    if (options.spaceAroundPipes && pipeNeedsSpaceBefore(cell)) out.append(' ');
                    appendColumnSpan(out, cell.columnSpan, cell.getInsideEndOffset(), cell.spanTrackedOffset);
                } else {
                    if (options.spaceAroundPipes && pipeNeedsSpaceBefore(cell)) out.append(' ');
                    appendColumnSpan(out, cell.columnSpan - 1, cell.getInsideEndOffset(), cell.spanTrackedOffset);
                }
            }

            if (row.afterOffset != NOT_TRACKED) {
                setTrackedOffsetIndex(row.afterOffset, out.offsetWithPending());
            }

            if (j > 0) out.line();
        }
    }

    private void appendColumnSpan(
            LineAppendable out,
            int span,
            int cellInsideEndOffset,
            int trackedSpanOffset
    ) {
        if (trackedSpanOffset == NOT_TRACKED) {
            out.append('|', span);
        } else {
            if (trackedSpanOffset == 0) {
                setTrackedOffsetIndex(cellInsideEndOffset + trackedSpanOffset, out.offsetWithPending());
                out.append('|', span);
            } else if (trackedSpanOffset < span) {
                out.append('|', trackedSpanOffset);
                setTrackedOffsetIndex(cellInsideEndOffset + trackedSpanOffset, out.offsetWithPending());
                out.append('|', span - trackedSpanOffset);
            } else {
                out.append('|', span);
                setTrackedOffsetIndex(cellInsideEndOffset + trackedSpanOffset, out.offsetWithPending());
            }
        }
    }

    private BasedSequence cellText(
            List<TableCell> cells,
            int index,
            boolean withTrackedOffset,
            boolean isHeader,
            int width,
            CellAlignment alignment,
            Ref<Integer> delta
    ) {
        TableCell cell = cells.get(index);
        TableCell adjustedCell = cell;
        BasedSequence text = cell.text;
        boolean needsPadding = cell.trackedTextOffset != NOT_TRACKED && cell.trackedTextOffset >= cell.text.length();
        boolean neededPrefix = false;

        if (cell.trackedTextOffset != NOT_TRACKED) {
            if (cell.trackedTextOffset > cell.text.length()) {
                // add padding spaces
                int suffixed = cell.trackedTextOffset - cell.text.length() - 1;
                text = text.append(RepeatedSequence.repeatOf(' ', suffixed));
            } else if (cell.trackedTextOffset < 0) {
                neededPrefix = true;
            }
        }

        int length = options.charWidthProvider.getStringWidth(text);
        if (options.adjustColumnWidth && (length < width || cell.trackedTextOffset > cell.text.length())) {
            if (!options.applyColumnAlignment || alignment == null || alignment == CellAlignment.NONE) {
                alignment = isHeader && options.leftAlignMarker != ADD ? CellAlignment.CENTER : CellAlignment.LEFT;
            } else if (isHeader && alignment == CellAlignment.LEFT && options.leftAlignMarker == DiscretionaryText.REMOVE) {
                alignment = CellAlignment.CENTER;
            }

            int diff = width - length;
            int spaceCount = (delta.value + diff) / options.spaceWidth;
            // NOTE: add extra space if accumulated adding extra space gives smaller abs error
            if (width > 0 && Math.abs(delta.value + diff - (spaceCount + 1) * options.spaceWidth) < Math.abs(delta.value + diff - spaceCount * options.spaceWidth)) {
                spaceCount++;
            }

            delta.value += diff - spaceCount * options.spaceWidth;

            switch (alignment) {
                case LEFT:
                    if (spaceCount > 0) {
                        text = text.append(PrefixedSubSequence.repeatOf(" ", spaceCount, text.getEmptySuffix()));
                    }

                    if (withTrackedOffset && needsPadding && cell.afterSpace) {
                        // if did not grow then move caret right
                        if (spaceCount <= 0) { adjustedCell = adjustedCell.withTrackedTextAdjust(1); }
                    }
                    break;

                case RIGHT:
                    if (spaceCount > 0) {
                        text = PrefixedSubSequence.repeatOf(" ", spaceCount, text);

                        if (withTrackedOffset && cell.trackedTextOffset != NOT_TRACKED) {
                            adjustedCell = cell.withTrackedOffset(maxLimit(text.length(), cell.trackedTextOffset + spaceCount));
                        }

                        if (withTrackedOffset && neededPrefix && cell.afterSpace) {
                            adjustedCell = adjustedCell.withTrackedTextAdjust(1);
                        }
                    }

                    if (withTrackedOffset && needsPadding && cell.afterSpace) {
                        if (spaceCount <= 0 || !cell.afterDelete) { adjustedCell = adjustedCell.withTrackedTextAdjust(1); }
                    }
                    break;

                case CENTER:
                    int count = spaceCount / 2;
                    if (spaceCount > 0) {
                        text = PrefixedSubSequence.repeatOf(" ", count, text).append(PrefixedSubSequence.repeatOf(" ", spaceCount - count, text.getEmptySuffix()));

                        if (withTrackedOffset && cell.trackedTextOffset != NOT_TRACKED) {
                            adjustedCell = cell.withTrackedOffset(maxLimit(text.length(), cell.trackedTextOffset + count));
                        }

                        if (withTrackedOffset && neededPrefix && cell.afterSpace) {
                            adjustedCell = adjustedCell.withTrackedTextAdjust(1);
                        }
                    } else {
                        if (withTrackedOffset && needsPadding && cell.afterSpace) {
                            adjustedCell = adjustedCell.withTrackedTextAdjust(1);
                        }
                    }
                    break;
            }
        }

        if (withTrackedOffset && adjustedCell.trackedTextOffset != NOT_TRACKED) {
            // replace with adjusted offset
            if (adjustedCell.trackedTextOffset > text.length()) {
                adjustedCell = adjustedCell.withTrackedOffset(text.length());
            }

            if (adjustedCell != cell) cells.set(index, adjustedCell);
        }

        return text;
    }

    private int spanWidth(int col, int columnSpan) {
        if (columnSpan > 1) {
            int width = 0;
            for (int i = 0; i < columnSpan; i++) {
                width += columnWidths[i + col];
            }
            return width;
        } else {
            return columnWidths[col];
        }
    }

    private int spanFixedWidth(BitSet unfixedColumns, int col, int columnSpan) {
        if (columnSpan > 1) {
            int width = 0;
            for (int i = 0; i < columnSpan; i++) {
                if (!unfixedColumns.get(i)) {
                    width += columnWidths[i + col];
                }
            }
            return width;
        } else {
            return unfixedColumns.get(col) ? 0 : columnWidths[col];
        }
    }

    private static class ColumnSpan {
        final int startColumn;
        final int columnSpan;
        final int width;
        int additionalWidth;

        public ColumnSpan(int startColumn, int columnSpan, int width) {
            this.startColumn = startColumn;
            this.columnSpan = columnSpan;
            this.width = width;
            this.additionalWidth = 0;
        }
    }

    private CellAlignment adjustCellAlignment(CellAlignment alignment) {
        switch (options.leftAlignMarker) {
            case ADD:
                if (alignment == null || alignment == CellAlignment.NONE) { alignment = CellAlignment.LEFT; }
                break;
            case REMOVE:
                if (alignment == CellAlignment.LEFT) alignment = CellAlignment.NONE;
                break;

            default:
                break;
        }
        return alignment;
    }

    private int aggregateTotalColumnsWithoutColumns(
            TableSection[] sections,
            BiFunction<Integer, Integer, Integer> aggregator,
            int... skipColumns
    ) {
        Integer[] columns = new Integer[] { null };

        forAllSectionsRows(0, Integer.MAX_VALUE, sections, (row, allRowsIndex, rows, index) -> {
            int iMax = row.cells.size();
            int count = 0;
            for (int i = 0; i < iMax; i++) {
                if (!ArrayUtils.contained(i, skipColumns)) count += row.cells.get(i).columnSpan;
            }
            if (count != 0) {
                columns[0] = aggregator.apply(columns[0], count);
            }
            return 0;
        });

        return columns[0] == null ? 0 : columns[0];
    }

    private int aggregateTotalColumnsWithoutRows(
            TableSection[] sections,
            BiFunction<Integer, Integer, Integer> aggregator,
            int... skipRows
    ) {
        Integer[] columns = new Integer[] { null };

        forAllSectionsRows(0, Integer.MAX_VALUE, sections, (row, allRowsIndex, rows, index) -> {
            if (!ArrayUtils.contained(allRowsIndex, skipRows)) {
                int totalColumns = row.getTotalColumns();
                if (totalColumns > 0) { columns[0] = aggregator.apply(columns[0], totalColumns); }
            }
            return 0;
        });

        return columns[0] == null ? 0 : columns[0];
    }

    private void forAllSectionsRows(
            int startIndex,
            int count,
            TableSection[] sections,
            TableRowManipulator manipulator
    ) {
        if (count <= 0) return;
        int remaining = count;
        int sectionIndex = startIndex;
        int allRowsIndex = startIndex;

        for (TableSection section : sections) {
            int currentIndex;

            if (sectionIndex >= section.rows.size()) {
                sectionIndex -= section.rows.size();
                continue;
            } else {
                currentIndex = sectionIndex;
                sectionIndex = 0;
            }

            while (currentIndex < section.rows.size()) {
                int result = manipulator.apply(section.rows.get(currentIndex), allRowsIndex, section.rows, currentIndex);
                if (result == TableRowManipulator.BREAK) return;
                if (result < 0) {
                    allRowsIndex -= result; // adjust for deleted rows
                    remaining += result;
                } else {
                    currentIndex += result + 1;
                    remaining--;
                }
                if (remaining <= 0) return;
                allRowsIndex++;
            }
        }
    }

    public static class IndexSpanOffset {
        final public int index;
        final public int spanOffset;

        public IndexSpanOffset(int index, int spanOffset) {
            this.index = index;
            this.spanOffset = spanOffset;
        }

        @Override
        public String toString() {
            return "IndexSpanOffset{" +
                    "index=" + index +
                    ", spanOffset=" + spanOffset +
                    '}';
        }
    }

    @Override
    public String toString() {
        // NOTE: show not simple name but name of container class if any
        return this.getClass().getName().substring(getClass().getPackage().getName().length() + 1) + "{" +
                "header=" + header +
                ",\nseparator=" + separator +
                ",\nbody=" + body +
                ",\ncaption=" + caption +
                ",\noptions=" + options +
                ",\ntrackedOffsets=" + trackedOffsets +
                "}";
    }
}
